Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add capability to write signal with unique samps_per_frame to wfdb.io.wrsamp #510

Open
wants to merge 9 commits into
base: main
Choose a base branch
from

Conversation

briangow
Copy link
Collaborator

This PR adds the capability for writing signals with unique samples per frame (samps_per_frame) to wfdb.io.wrsamp. This is typically the function that is used to write WFDB files. This was previously only possible to do by creating a Record first and using its wrsamp method to do the write.

@bemoody , I do feel like checking that the frames were uniform took a bit of extra code. I'm happy to leave it like this but also open to discussing other approaches.

I've added a couple of tests to check that this continues to work as expected.

@briangow briangow requested a review from bemoody October 16, 2024 22:00
if p_signal is not None and d_signal is not None:
signal_list = [p_signal, d_signal, e_p_signal, e_d_signal]
signals_set = sum(1 for var in signal_list if var is not None)
if signals_set != 1:
Copy link
Member

@tompollard tompollard Oct 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this intentionally changing behavior for calls where p_signal=None and d_signal=None? In the past, this would evaluate to False when e_p_signal=None and e_d_signal=None, but it now evaluates to True.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tompollard , if all signals are passed as None a failure will occur further down the line. @bemoody , do we want to allow all signals to be None when calling wfdb.io.wrsamp? If we don't want to allow that then I will leave the code as I have it which requires that one of the signals is set.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If passing None for both signals (p_signal and d_signal) previously resulted in an error, then the new behavior is an improvement. Unless people were intentioanlly calling the function with both signals set to None previously, this change isn't a problem.

# required features.

# If samps_per_frame is a list, check that it aligns as expected with the channels in the signal
if len(samps_per_frame) > 1:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If samps_per_frame is not a list (e.g. it's an int), this will error out.

len(4)
TypeError: object of type 'int' has no len()

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

raise TypeError("Unsupported signal format. Must be ndarray or list of lists.")

# Check that the number of channels matches the number of samps_per_frame entries
if num_sig_channels != len(samps_per_frame):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as above, len(samps_per_frame) will error if samps_per_frame is an int.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

wfdb/io/record.py Outdated Show resolved Hide resolved
@briangow
Copy link
Collaborator Author

@tompollard , if this failing test is of concern, could you guide me on what to do to resolve it:

would reformat /home/runner/work/wfdb-python/wfdb-python/wfdb/io/record.py
2 files would be reformatted, 36 files would be left unchanged.

@tompollard
Copy link
Member

tompollard commented Oct 18, 2024

@tompollard , if this failing test is of concern, could you guide me on what to do to resolve it:

It looks like the style checks are failing (https://github.com/MIT-LCP/wfdb-python/actions/runs/11408864264/job/31747939807?pr=510). To fix, you'll need to make sure that the code conforms to black (install black, run black over the code to reformat).

@bemoody
Copy link
Collaborator

bemoody commented Oct 21, 2024

The following raises an error:

>>> wfdb.wrsamp("xxx", fs=500, units=["mV"], sig_name=["I"], fmt=["16"],
                e_p_signal=[numpy.array([0.0, 1.0, 2.0])],
                adc_gain=[1.0], baseline=[0])
TypeError: 'NoneType' object is not subscriptable

In this case, wrsamp should raise an exception to say that if e_p_signal is provided, then samps_per_frame must be too.

@bemoody
Copy link
Collaborator

bemoody commented Oct 21, 2024

The following raises an error:

>>> wfdb.wrsamp("xxx", fs=500, units=["mV"], sig_name=["I"], e_p_signal=[numpy.array([0.0, 1.0, 2.0])], fmt=["16"], samps_per_frame=[3])
TypeError: ufunc 'isinf' not supported for the input types, and the inputs could not be safely coerced to any supported types according to the casting rule ''safe''

The problem here is that calc_adc_params doesn't handle the expanded case. calc_adc_params should use the same method to generate adc_gain and baseline as it does in the non-expanded case.

I think it should be straightforward to restructure calc_adc_params to take an expanded argument and to use e_p_signal instead of p_signal where appropriate.

@briangow
Copy link
Collaborator Author

The following raises an error:

>>> wfdb.wrsamp("xxx", fs=500, units=["mV"], sig_name=["I"], fmt=["16"],
                e_p_signal=[numpy.array([0.0, 1.0, 2.0])],
                adc_gain=[1.0], baseline=[0])
TypeError: 'NoneType' object is not subscriptable

In this case, wrsamp should raise an exception to say that if e_p_signal is provided, then samps_per_frame must be too.

@bemoody , thanks for catching this. I think this also applies for e_d_signal. I've added a check to make sure samps_per_frame is supplied when writing these expanded signals.

I've also updated the check to make sure samps_per_frame aligns with the channels in the signal, to consider the case when samps_per_frame is an int instead of a list.

Finally, I think we should allow passing samps_per_frame with a 1 for each channel to wrsamp when a mixed frequency signal is read in with smooth_frames=True:

rec = wfdb.io.rdrecord('/wfdb-python/sample-data/mixedsignals', smooth_frames=True)

print(rec.samps_per_frame)
[4, 4, 4, 2, 2, 1]

wfdb.io.wrsamp('written_mixedsignals', fs = rec.fs, units=rec.units, sig_name=rec.sig_name, base_date=rec.base_date, base_time=rec.base_time, comments=rec.comments, p_signal=rec.p_signal, samps_per_frame=[1,1,1,1,1,1], baseline=rec.baseline, adc_gain=rec.adc_gain, fmt=rec.fmt)

Please let me know if you disagree, or have other thoughts with respect to this case.

@briangow
Copy link
Collaborator Author

The following raises an error:

>>> wfdb.wrsamp("xxx", fs=500, units=["mV"], sig_name=["I"], e_p_signal=[numpy.array([0.0, 1.0, 2.0])], fmt=["16"], samps_per_frame=[3])
TypeError: ufunc 'isinf' not supported for the input types, and the inputs could not be safely coerced to any supported types according to the casting rule ''safe''

The problem here is that calc_adc_params doesn't handle the expanded case. calc_adc_params should use the same method to generate adc_gain and baseline as it does in the non-expanded case.

I think it should be straightforward to restructure calc_adc_params to take an expanded argument and to use e_p_signal instead of p_signal where appropriate.

@bemoody , this is being addressed in a separate PR: #512 .

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants